在現代 Vue 3 應用中,有效管理全局狀態和本地存儲是構建可靠且高性能應用的關鍵。本文將深入探討如何使用 Pinia 結合多種先進技術來實現全面的狀態管理解決方案。我們將涵蓋永久存儲、離線緩存、網絡狀態管理等主題,並整合 Zod、Vee-Validate、@vueuse/core 等工具,打造一個強大且靈活的狀態管理系統。
首先,我們創建一個基本的 Pinia store,使用 setup store 語法:
(如果不知道什麼是 definePrivateState
請看 Day11的觀念)
(檔案:src/stores/useAuthStore.ts
)
import { computed } from 'vue'
import { acceptHMRUpdate } from 'pinia';
import { definePrivateState } from './privateState';
import { Nullable, UserSchema } from '../schemas/user.schema';
import { useStorage } from '@vueuse/core';
export interface UseAuthStorePrivateState {
}
export const useAuthStore = definePrivateState('useAuthStore', (): UseAuthStorePrivateState => {
return {
}
}, (privateState, router) => {
const user = useStorage<Nullable<UserSchema>>('user', null, localStorage, {
serializer: {
read: JSON.parse,
write: JSON.stringify
}
});
// getters::
const isLoggedIn = computed<boolean>(() => user.value !== null);
// methods::
const setUser = (currentUser: Nullable<UserSchema>): void => {
if (currentUser === null) return;
user.value = currentUser;
};
return {
// getters::
isLoggedIn,
// methods::
setUser
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot));
}
型別的部分:
(檔案: src/schemas/user.schema.ts
)
import * as zod from 'zod';
export const userSchema = zod.object({
id: zod.number(),
name: zod.string(),
email: zod.string().email(),
});
export type UserSchema = zod.infer<typeof userSchema>;
export type Nullable<T> = T | null;
為了實現單向數據流,我們可以創建一個私有 store 來處理內部狀態:
import { computed } from 'vue'
import { acceptHMRUpdate } from 'pinia';
import { definePrivateState } from './privateState';
import { Nullable, UserSchema } from '../schemas/user.schema';
import { useStorage } from '@vueuse/core';
export interface UseAuthStorePrivateState {
lastUpdated: Nullable<Date>;
}
export const useAuthStore = definePrivateState('useAuthStore', (): UseAuthStorePrivateState => {
return {
lastUpdated: null, // 這裡增加最後時間
}
}, (privateState) => {
const user = useStorage<Nullable<UserSchema>>('user', null, localStorage, {
serializer: {
read: JSON.parse,
write: JSON.stringify
}
});
// getters::
const isLoggedIn = computed<boolean>(() => user.value !== null);
// methods::
const setUser = (currentUser: Nullable<UserSchema>): void => {
if (currentUser === null) return;
user.value = currentUser;
privateState.lastUpdated = new Date(); // 確保每次更新狀態可以被偵測
};
return {
// getters::
isLoggedIn,
// methods::
setUser
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot));
}
localStorage
可以應用大部分的狀況,但仍有些極端狀態用 indexedDB 較為合適我們這裡用集成好的 indexedDB
解決方法,安裝 idb
,為了使原本的 code 更為簡潔
bun add idb
接下來,我們將實現一個使用 IndexedDB 的 store:
(檔案: src/stores/useOfflineStore.ts
)
import { acceptHMRUpdate } from 'pinia';
import { definePrivateState } from './privateState';
import { openDB } from 'idb';
import { Nullable, UserOffLineDB, UserOffLineSchema, UserSchema } from '../schemas/user.schema';
export interface useOfflineStorePrivateState {
key: 'offLineData',
userDB: Nullable<UserOffLineDB>;
}
export const useOfflineStore = definePrivateState('useOfflineStore', (): useOfflineStorePrivateState => {
return {
key: 'offLineData',
userDB: null,
}
}, privateState => {
// getters::
// methods::
const initUserDB = async (): Promise<void> => {
privateState.userDB = await openDB<UserOffLineSchema>('user-demo-db', 1, {
upgrade(db) {
db.createObjectStore(privateState.key)
}
})
};
const setOffLineUser = async (key: string, value: UserSchema): Promise<void> => {
const db = privateState.userDB;
if (!db) await initUserDB();
if (db === null) throw new Error('some unexpected error');
await db.put('offLineData', value, key);
};
const getOffLineUser = async (key: string): Promise<UserSchema | undefined> => {
const db = privateState.userDB;
if (!db) await initUserDB();
if (db === null) throw new Error('some unexpected error');
return await db.get('offLineData', key);
};
return {
// methods::
initUserDB,
setOffLineUser,
getOffLineUser
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useOfflineStore, import.meta.hot));
}
(檔案: src/schemas/user.schema.ts
)
import { DBSchema, IDBPDatabase } from 'idb';
// 前面的部分一樣...
export interface OffLineDBSchema<T> extends DBSchema {
offLineData: {
key: string;
value: T,
}
}
export type UserOffLineSchema = OffLineDBSchema<UserSchema>;
export type UserOffLineDB = IDBPDatabase<UserOffLineSchema>;
創建一個 store 來管理網絡狀態和離線數據:
備註
: 如果對 loadingStatus
的狀態不了解的話,可以參考 Day 15的實作
(檔案: src/stores/useUserStore.ts
)
import { watch } from 'vue';
import { acceptHMRUpdate } from 'pinia';
import { definePrivateState } from './privateState';
import { useOfflineStore } from './useOfflineStore';
import { useUserApi } from '../composables/useUserApi';
import { useApiFetch } from '../composables/useApiFetch';
import { useNetwork } from '@vueuse/core';
import { useAuthStore } from './useAuthStore';
import { useLoadingStore } from './useLoadingStore';
import { LoadingStatus } from '../schemas/user.schema';
export const useUserStore = definePrivateState('useUserStore', () => {
return {
}
}, () => {
// composables::
const { getUserApi } = useUserApi(useApiFetch);
const { isOnline } = useNetwork();
// stores::
const { setOffLineUser, getOffLineUser } = useOfflineStore();
const { isLoadingStatusExist, addLoadingStatus, removeLoadingStatus } = useLoadingStore();
const { setUser } = useAuthStore();
// methods::
const getUserWhenOnline = async (): Promise<boolean> => {
if (isLoadingStatusExist(LoadingStatus.GetUser)) return false;
addLoadingStatus(LoadingStatus.GetUser);
try {
const { data, error } = await getUserApi();
if (error.value) {
throw new Error('failed to get user');
}
setUser(data.value);
if (data.value) {
// 保存數據到 IndexedDB 以供離線使用
await setOffLineUser('demo-user', data.value);
}
return data.value !== null;
} catch (error) {
if (error instanceof Error) {
console.error(error);
}
return false;
} finally {
removeLoadingStatus(LoadingStatus.GetUser);
}
};
const getUserWhenOffLine = async (): Promise<void> => {
const user = await getOffLineUser('demo-user');
if (!user) return;
setUser(user);
};
const triggerSetUser = async (): Promise<void> => {
isOnline.value && await getUserWhenOnline();
!isOnline.value && await getUserWhenOffLine(); // 如果離線,從 IndexedDB 獲取數據
};
watch(isOnline, (newValue) => {
console.log(newValue ? 'Back online' : 'Gone offline')
// 這裡可以添加在線狀態變化時的邏輯
})
return {
// getters::
isOnline,
// methods::
triggerSetUser
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot));
}
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useUserStore } from '../stores/useUserStore';
import { useAuthStore } from '../stores/useAuthStore';
const userStore = useUserStore();
const { triggerSetUser } = userStore;
const { isOnline } = storeToRefs(userStore);
const { user } = storeToRefs(useAuthStore());
</script>
<template>
<div>
<p>Network status: {{ isOnline ? 'Online' : 'Offline' }}</p>
<button aria-label="fetch user data" @click="triggerSetUser" border-none px-3 py-2 rounded-md cursor-pointer box-border text="hover:white" bg="blue-400 hover:blue-800">Fetch User Data</button>
<div v-if="user">
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
</div>
</div>
</template>
我們探討了如何在 Pinia 中管理 Vue 3 應用的全局狀態和本地存儲。我們實現了:
useStorage
實現永久存儲。這種全面的方法不僅提供了強大的狀態管理能力,還確保了應用在離線環境下的可用性。通過結合 Pinia、@vueuse/core、IndexedDB,我們創建了一個健壯的系統,能夠處理各種網絡情況和數據持久化需求。
在實際應用中,這種方法可以進一步擴展以處理更複雜的場景,如數據同步、衝突解決等。持續優化和改進這個系統將幫助您構建更可靠、更高效的 Vue 應用。